﻿// <copyright file="MovieReviewBase.cs" company="Microsoft Corporation">
// Copyright (c) 2009 Microsoft Corporation All Rights Reserved
// </copyright>
// <author>Michael S. Scherotter</author>
// <email>mischero@microsoft.com</email>
// <date>2009-04-08</date>
// <summary>Movie Reivew API base Class</summary>

namespace NYTimes.Core
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Linq;
    using System.Net;
    using System.Text;
    using System.Windows;
    using System.Windows.Media.Imaging;
    using System.Xml.Linq;
    using NYTimes.Data;
    using Synergist;

    /// <summary>
    /// New York Times Movie Review API Base class
    /// </summary>
    /// <remarks>See <a href="http://developer.nytimes.com/docs/movie_reviews_api" target="_blank">
    /// this article</a> for more information about the NYT Movie Review API.
    /// <list type="table">
    /// <listheader>
    /// <term>Rate Limiting</term>
    /// </listheader>
    ///     <item><term>Default Queries Per Second</term><description>8</description></item>
    ///     <item><term>Resource Key</term><description>NYTMoviesQPS</description></item>
    ///     <item><term>Overriding default QPS</term><description>To Override the rate limiting add 
    ///     a namespace declaration <c>xmlns:sys="clr-namespace:System;assembly=mscorlib"</c> and 
    ///     an integer resource to the App.Xaml Application.Resources like this: 
    ///     <c>&lt;sys:Int32 x:Key="NYTMoviesQPS"&gt;16&lt;/sys:Int32&gt;</c></description>
    ///     </item>
    /// </list>
    /// </remarks>
    public abstract class MovieReviewBase : INotifyPropertyChanged
    {
        #region Fields
        /// <summary>
        /// the Application resource key where the API Key is stored
        /// </summary>
        internal const string Key = "NYTMovieReviewsKey";

        /// <summary>
        /// 8 QPS rate limiting
        /// </summary>
        private static RateLimiting limiting = new RateLimiting("NYTMovies", 8);

        /// <summary>
        /// the map of review order enums to string
        /// </summary>
        private static Dictionary<MovieReviewOrder, string> reviewOrderMap = new Dictionary<MovieReviewOrder, string>();

        /// <summary>
        /// The offset
        /// </summary>
        private int offset;

        /// <summary>
        /// Are the results loading
        /// </summary>
        private bool isLoading;

        /// <summary>
        /// the total results
        /// </summary>
        private int totalResults;

        /// <summary>
        /// the number of results to get
        /// </summary>
        private int resultsToGet;

        /// <summary>
        /// the copyright text
        /// </summary>
        private string copyright;

        /// <summary>
        /// the web client
        /// </summary>
        private WebClient client;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the MovieReviewBase class.
        /// </summary>
        protected MovieReviewBase()
        {
            if (Application.Current.Resources.Contains(Key))
            {
                this.ApiKey = Application.Current.Resources[Key] as string;
            }

            this.Results = new ObservableCollection<MovieReview>();

            if (SampleData.IsInDesignTool)
            {
                var xml = NYTimes.Core.SampleData.GetSampleData("MovieReviewsByKeyword.xml");

                if (!string.IsNullOrEmpty(xml))
                {
                    this.ParseXml(xml);
                }
            }
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Property changed event handler
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the API Key
        /// </summary>
        [Description("The NYT Movie Review API Key.")]
        public string ApiKey { get; set; }

        /// <summary>
        /// Gets or sets the number of results to get
        /// </summary>
        [Description("The number of results to return.")]
        public int ResultsToGet
        {
            get
            {
                return this.resultsToGet;
            }

            set
            {
                if (this.resultsToGet != value)
                {
                    this.resultsToGet = value;

                    this.TriggerPropertyChanged("ResultsToGet");

                    this.UpdateResults();
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether the results are loading
        /// </summary>
        public bool IsLoading 
        {
            get
            {
                return this.isLoading;
            }

            private set
            {
                if (this.isLoading != value)
                {
                    this.isLoading = value;

                    this.TriggerPropertyChanged("IsLoading");
                }
            }
        }

        /// <summary>
        /// Gets the offset
        /// </summary>
        public int Offset
        {
            get
            {
                return this.offset;
            }

            private set
            {
                this.offset = value;

                if (!SampleData.IsInDesignTool)
                {
                    if (this.offset == 0)
                    {
                        this.Results.Clear();
                    }

                    this.FetchResults();
                }
            }
        }

        /// <summary>
        /// Gets the results
        /// </summary>
        public ObservableCollection<MovieReview> Results { get; private set; }

        /// <summary>
        /// Gets the copyright message
        /// </summary>
        public string Copyright
        {
            get
            {
                return this.copyright;
            }

            private set
            {
                this.copyright = value;

                this.TriggerPropertyChanged("Copyright");
            }
        }

        /// <summary>
        /// Gets the total number of results
        /// </summary>
        public int TotalResults
        {
            get
            {
                return this.totalResults;
            }

            private set
            {
                this.totalResults = value;

                this.TriggerPropertyChanged("TotalResults");
            }
        }

        /// <summary>
        /// Gets the logo for the New York Times
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1822", Justification = "This is exposed to a Silverlight application.")]
        public BitmapImage Logo
        {
            get
            {
                var streamResource = Application.GetResourceStream(
    new Uri("NYTimesSilverlightKit;component/Images/Logo.png", UriKind.Relative));

                var image = new BitmapImage();

#if SILVERLIGHT
                image.SetSource(streamResource.Stream);
#else
                image.StreamSource = streamResource.Stream;
#endif

                return image;
            }
        }

        /// <summary>
        /// Gets the review order map
        /// </summary>
        internal static IDictionary<MovieReviewOrder, string> ReviewOrderMap
        {
            get
            {
                if (reviewOrderMap.Count == 0)
                {
                    if (reviewOrderMap.Count == 0)
                    {
                        reviewOrderMap[MovieReviewOrder.DVDReleaseDate] = "by-dvd-release-date";
                        reviewOrderMap[MovieReviewOrder.OpeningDate] = "by-opening-date";
                        reviewOrderMap[MovieReviewOrder.PublicationDate] = "by-publication-date";
                        reviewOrderMap[MovieReviewOrder.Title] = "by-title";
                    }
                }

                return reviewOrderMap;
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Generate the REST URI
        /// </summary>
        /// <returns>the REST URI or null</returns>
        public abstract Uri GenerateUri();

        /// <summary>
        /// Update the results
        /// </summary>
        public void UpdateResults()
        {
            this.Offset = 0;
        }

        /// <summary>
        /// append a limiter variable
        /// </summary>
        /// <param name="builder">the string builder</param>
        /// <param name="field">the field to query</param>
        /// <param name="parameterName">the parameter name</param>
        protected static void AppendLimiter(StringBuilder builder, bool? field, string parameterName)
        {
            if (builder == null)
            {
                throw new ArgumentNullException("builder");
            }

            if (field.HasValue)
            {
                if (field.Value)
                {
                    builder.AppendFormat(CultureInfo.InvariantCulture, "&{0}=Y", parameterName);
                }
                else
                {
                    builder.AppendFormat(CultureInfo.InvariantCulture, "&{0}=N", parameterName);
                }
            }
        }

        /// <summary>
        /// Trigger Property changed event
        /// </summary>
        /// <param name="propertyName">the property name</param>
        protected void TriggerPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Appends the offset to a the string builder
        /// </summary>
        /// <param name="builder">the string builder</param>
        protected void AppendOffset(StringBuilder builder)
        {
            if (this.Offset > 0)
            {
                if (builder == null)
                {
                    throw new ArgumentNullException("builder");
                }

                builder.AppendFormat(CultureInfo.InvariantCulture, "&offset={0}", this.Offset);
            }
        }

        #endregion

        #region Implementation

        /// <summary>
        /// parse an element for a DateTime
        /// </summary>
        /// <param name="element">the XML Element</param>
        /// <param name="name">the name of the child element</param>
        /// <returns>a Nullable DateTime</returns>
        private static DateTime? ParseDate(XElement element, string name)
        {
            string dateString = element.Element(name).Value;

            if (string.IsNullOrEmpty(dateString))
            {
                return new DateTime?();
            }

            DateTime dateTime;

            if (DateTime.TryParse(dateString, out dateTime))
            {
                return new DateTime?(dateTime);
            }

            System.Diagnostics.Debug.WriteLine("Cannot parse {0} as a DateTime.", dateString);

            return new DateTime?();
        }

        /// <summary>
        /// Create a Link
        /// </summary>
        /// <param name="element">the XML Element</param>
        /// <returns>a Link object</returns>
        private static Link CreateLink(XElement element)
        {
            return new Link
            {
                LinkType = element.Attribute("type").Value,
                SuggestedLinkText = element.Element("suggested_link_text").Value,
                Url = new Uri(element.Element("url").Value, UriKind.Absolute),
            };
        }

        /// <summary>
        /// Fetch the results
        /// </summary>
        private void FetchResults()
        {
            if (this.client != null)
            {
                this.client.CancelAsync();

                this.client = null;
            }

            if (string.IsNullOrEmpty(this.ApiKey))
            {
                return;
            }

            var uri = this.GenerateUri();

            if (uri == null)
            {
                return;
            }

            this.IsLoading = true;

            System.Diagnostics.Debug.WriteLine(uri);

            this.client = new WebClient();

            this.client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(this.OnResultsReturned);

            limiting.MakeWebRequest(this.client, uri);
        }

        /// <summary>
        /// results are returned
        /// </summary>
        /// <param name="sender">the web client</param>
        /// <param name="e">the download string completed event arguments</param>
        private void OnResultsReturned(object sender, DownloadStringCompletedEventArgs e)
        {
            this.client = null;

            if (e.Error != null)
            {
                if (!e.Cancelled)
                {
                    System.Diagnostics.Debug.WriteLine(e.Error.Message);

                    this.IsLoading = false;
                }

                return;
            }

            var xml = e.Result;

            this.ParseXml(xml);

            if (this.Results.Count < this.TotalResults && this.Results.Count < this.ResultsToGet)
            {
                this.Offset += 20;
            }
            else
            {
                this.IsLoading = false;
            }
        }

        /// <summary>
        /// Parse the XML 
        /// </summary>
        /// <param name="xml">the XML returned from the service call</param>
        private void ParseXml(string xml)
        {
            var doc = XDocument.Parse(xml);

            var resultSet = doc.Element("result_set");

            this.TotalResults = int.Parse(resultSet.Element("num_results").Value, CultureInfo.InvariantCulture);
            this.Copyright = resultSet.Element("copyright").Value;

            var reviews = from review in doc.Descendants("review")
                          select new MovieReview()
                          {
                              Byline = review.Element("byline").Value,
                              CapsuleReview = review.Element("capsule_review") == null ? null : Platform.HtmlDecode(review.Element("capsule_review").Value),
                              CriticsPick = review.Element("critics_pick").Value == "Y",
                              DisplayTitle = review.Element("display_title").Value,
                              DVDReleaseDate = ParseDate(review, "dvd_release_date"),
                              Headline = review.Element("headline") == null ? null : review.Element("headline").Value,
                              Id = int.Parse(review.Attribute("nyt_movie_id").Value, CultureInfo.InvariantCulture),
                              Link = CreateLink(review.Element("link")),
                              MPAARating = review.Element("mpaa_rating") == null ? null : review.Element("mpaa_rating").Value,
                              OpeningDate = ParseDate(review, "opening_date"),
                              PublicationDate = ParseDate(review, "publication_date").Value,
                              RelatedUrls = from link in review.Element("related_urls").Elements("link")
                                            select CreateLink(link),
                              SEOName = review.Element("seo_name").Value,
                              SortName = review.Element("sort_name").Value,
                              SummaryShort = review.Element("summary_short") == null ? null : review.Element("summary_short").Value,
                              ThousandBest = review.Element("thousand_best").Value == "Y",
                              Thumbnail = review.Element("multimedia") == null ? null : new BitmapImage(new Uri(review.Element("multimedia").Element("resource").Element("src").Value, UriKind.Absolute))
                          };

            foreach (var review in reviews)
            {
                this.Results.Add(review);
            }
        }

        #endregion
    }
}
